// @ts-check

/**
 * @typedef { [x: number, y: number] } Point
 */

/**
 * @typedef { [Point, Point] } Line
 */

/**
 * @typedef { [source: number, target: number] } AngleR
 */

/**
 * @typedef { "none" | "current" | "history" } BoxType
 */

/**
 * @typedef { { [state in string]: Point } } StateMap
 */

/**
 * @typedef { { source: string; target: string; angles?: AngleR } } ArrowLine
 */

/**
 * 按高与角度求边长
 * @param { number } height
 * @param { number } angle
 */
function getSideByHeightAngle(height, angle) {
  return height / Math.cos(angle * (Math.PI / 180));
}


/**
 * 按高与角度求宽度
 * @param { number } height
 * @param { number } angle
 */
function getWidthByHeightAngle(height, angle) {
  return height / Math.tan(angle * (Math.PI / 180));
}


/**
 * 计算点角度
 * @param { Point } pointA
 * @param { Point } pointB
 */
function getPointAngle(pointA, pointB) {
  const [x1, y1] = pointA;
  const [x2, y2] = pointB;
  const def_x = x2 - x1;
  const def_y = y2 - y1;
  const value = Math.atan(def_x / def_y);
  if (isNaN(value)) {
    return 0;
  }
  let angle = value / (Math.PI / 180);
  if (def_y >= 0) {
    angle = 90 - angle;
  }
  if (def_y < 0) {
    angle = 270 - angle;
  }
  return angle;
}


/**
 * 旋转点
 * @param { Point } point
 * @param { number } length
 * @param { number } angle
 * @return { Point }
 */
function rotatePoint(point, length, angle) {
  const [x1, y1] = point;
  const a = angle * (Math.PI / 180);
  return [Math.cos(a) * length + x1, Math.sin(a) * length + y1];
}


/**
 * 旋转相对点
 * @param { Point } sourcePoint
 * @param { Point } point
 * @param { number } angle
 * @return { Point }
 */
function rotatePointByPoint(sourcePoint, point, angle) {
  const [x1, y1] = sourcePoint;
  const [x2, y2] = point;
  const dx = x2 - x1;
  const dy = y2 - y1;
  const a = Math.atan2(dy, dx) + angle * (Math.PI / 180);
  const r = Math.sqrt(dx * dx + dy * dy);
  return [Math.cos(a) * r + x1, Math.sin(a) * r + y1];
}


/**
 * 求线条系数
 * @param { Line } line
 * @returns { [a: number, b: number, c: number] }
 */
function getLineCoefficient([[x1, y1], [x2, y2]]) {
  const a = y2 - y1;
  const b = x1 - x2;
  const c = x2 * y1 - x1 * y2
  return [a, b, c];
}


/**
 * 求线条交点
 * @param { Line } lineA
 * @param { Line } lineB
 * @returns { Point }
 */
function getLineIntersectionPoint(lineA, lineB) {
  const [a1, b1, c1] = getLineCoefficient(lineA);
  const [a2, b2, c2] = getLineCoefficient(lineB);
  const x = (c2 * b1 - c1 * b2) / (a1 * b2 - a2 * b1);
  const y = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1);
  return [x, y];
}


/**
 * 计算状态框边缘点
 * @param { Point } pointA
 * @param { Point } pointB
 */
function calcStateBoxEdgePoint(pointA, pointB) {
  const smallAngle = getPointAngle([0, 0], [40, 20]);
  const bigAngle = 90 - smallAngle;
  const dubleBigAngle = bigAngle * 2;
  const startAngle = smallAngle;
  const endAngle = startAngle + dubleBigAngle;

  const saveAngle = getPointAngle(pointA, pointB);
  let angle = saveAngle;
  if (angle > 180) {
    angle = angle - 180;
  }

  let side = null;

  if (angle >= startAngle && angle <= endAngle) {
    angle -= smallAngle;
    if (angle > bigAngle) {
      angle -= bigAngle;
    } else {
      angle = bigAngle - angle;
    }
    side = getSideByHeightAngle(20, angle);
  } else {
    if (angle > endAngle) {
      angle -= endAngle;
      angle = smallAngle - angle;
    }
    side = getSideByHeightAngle(40, angle);
  }

  return rotatePoint(pointA, side, saveAngle);
}


/**
 * 绘制背景
 * @param { HTMLCanvasElement } canvas
 * @param { CanvasRenderingContext2D } context
 * @param { number } wdith
 * @param { number } height
 * @param { number } zoom
 */
function drawBackground(canvas, context, wdith, height, zoom) {
  canvas.style.width = `${wdith}px`;
  canvas.style.height = `${height}px`;

  canvas.width = wdith * zoom;
  canvas.height = height * zoom;

  context.imageSmoothingEnabled = false;

  context.scale(zoom, zoom);

  context.save();
  context.fillStyle = "#f0f0f0";
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.restore();
}


/**
 * 圆角矩形
 * @param { CanvasRenderingContext2D } context
 * @param { number } x
 * @param { number } y
 * @param { number } w
 * @param { number } h
 * @param { number } r
 * @param { boolean } [fill]
 * @param { boolean } [border]
 */
function drawRoundBox(context, x, y, w, h, r, fill, border) {
  context.save();
  if (w >= r * 2) {
    context.beginPath();
    context.moveTo(x + r, y);
    context.arcTo(x + w, y, x + w, y + h, r);
    context.arcTo(x + w, y + h, x, y + h, r);
    context.arcTo(x, y + h, x, y, r);
    context.arcTo(x, y, x + w, y, r);
    if (fill) { context.fill(); }
    if (border) { context.stroke(); }
  } else {
    var left = x, right = x + w, top = y, bottom = y + h;
    context.beginPath();
    context.moveTo(right, bottom);
    context.arcTo(left, bottom, left, top, r);
    context.arcTo(left, top, right, top, r);
    context.lineTo(right, top);
    context.clip();
    context.beginPath();
    context.moveTo(left, top);
    context.arcTo(right, top, right, bottom, r);
    context.arcTo(right, bottom, left, bottom, r);
    context.lineTo(left, bottom);
    context.clip();
    if (fill) {
      context.fill();
    }
    if (border) {
      context.stroke();
      context.beginPath();
      context.moveTo(right, bottom);
      context.arcTo(left, bottom, left, top, r);
      context.arcTo(left, top, right, top, r);
      context.lineTo(right, top);
      context.stroke();
    }
  }
  context.restore();
}


/**
 * 绘制状态框
 * @param { CanvasRenderingContext2D } context
 * @param { Point } point
 * @param { string } name
 * @param { BoxType } type
 */
function drawStateBox(context, [x, y], name, type) {
  const width = 80;
  const height = 40;

  const left = x - width / 2;
  const top = y - height / 2;

  context.save();
  context.strokeStyle = "#c9c9c9";
  context.fillStyle = "#c9c9c9";
  drawRoundBox(context, left + 3, top + 3, width, height, 8, true, true);
  context.restore();

  context.save();
  context.strokeStyle = "#8198b2";
  context.lineWidth = 2;
  if (type === "current") {
    if (name === "Unspecified") {
      context.fillStyle = "#8198b2";
    } else {
      context.fillStyle = "#97c1ec";
    }
  } else if (type === "history") {
    context.fillStyle = "#b1d29d";
  } else {
    context.fillStyle = "#ffffff";
  }
  drawRoundBox(context, left, top, width, height, 8, true, true);
  context.restore();

  context.save();
  context.fillStyle = "#000000";
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.font = "12px System";
  context.fillText(name, x, y + 2);
  context.restore();
}


/**
 * 绘制箭头
 * @param { CanvasRenderingContext2D } context
 * @param { number } x
 * @param { number } y
 * @param { number } angle
 */
function drawArrow(context, x, y, angle) {
  context.save();
  context.translate(x, y)
  context.rotate(angle * (Math.PI / 180));

  context.fillStyle = "#000";
  context.beginPath();
  context.moveTo(0, 0);
  context.lineTo(-10, -4);
  context.lineTo(-9, 0);
  context.lineTo(-10, 4);
  context.closePath();
  context.fill();

  context.restore();
}


/**
 * 绘制箭头线条
 * @param { CanvasRenderingContext2D } context
 * @param { Point[] } points
 */
function drawArrowLine(context, ...points) {
  if (!points || points.length < 2) {
    throw new Error("绘制箭头参数异常");
  }

  context.save();
  context.lineWidth = 1;
  context.strokeStyle = "#000";
  context.beginPath();
  context.moveTo(points[0][0], points[0][1]);
  for (const point of points.slice(1)) {
    context.lineTo(point[0], point[1]);
  }
  context.stroke();
  context.restore();

  const lastIndex = points.length - 1;

  const angle = getPointAngle(points[lastIndex - 1], points[lastIndex]);

  drawArrow(context, ...points[points.length - 1], angle);
}


/**
 * 绘制状态框连接线
 * @param { CanvasRenderingContext2D } context
 * @param { Point } stateBoxA
 * @param { Point } stateBoxB
 * @param { AngleR } [angles]
 * @param { Line } [line]
 */
function drawEdgeArrowLine(context, stateBoxA, stateBoxB, angles, line) {

  const angleA = getPointAngle(stateBoxA, stateBoxB);
  const angleB = getPointAngle(stateBoxB, stateBoxA);

  /** @type { Point[] } */
  const points = [stateBoxA];

  if (angles) {
    /** @type { Line }} */
    const lineA = [stateBoxA, rotatePoint(stateBoxA, 1000, angleA + angles[0])];
    /** @type { Line }} */
    const lineB = [stateBoxB, rotatePoint(stateBoxB, 1000, angleB + angles[1])];

    if (line) {
      points.push(getLineIntersectionPoint(lineA, line));
      points.push(getLineIntersectionPoint(line, lineB));
    } else {
      points.push(getLineIntersectionPoint(lineA, lineB));
    }

  }

  points.push(stateBoxB);

  const lastIndex = points.length - 1;

  points[0] = calcStateBoxEdgePoint(points[0], points[1]);
  points[lastIndex] = calcStateBoxEdgePoint(points[lastIndex], points[lastIndex - 1]);

  drawArrowLine(context, ...points);
}


/**
 * 判断状态矩形类型
 * @param { string } name
 * @param { string } currentState
 * @param { string[] } historeStates
 * @return { BoxType }
 */
function judgeBoxType(name, currentState, historeStates) {
  if (currentState === name) {
    return "current";
  }
  else if (historeStates.includes(name)) {
    return "history";
  }
  else {
    return "none";
  }
}


/**
 * 绘制状态框表
 * @param { CanvasRenderingContext2D } context
 * @param { string } currentState
 * @param { string[] } historeStates
 * @param { StateMap } stateBoxMap
 * @param { ArrowLine[] } arrowLines
 */
function dragStateBoxMap(context, currentState, historeStates, stateBoxMap, arrowLines) {
  for (const name in stateBoxMap) {
    const boxType = judgeBoxType(name, currentState, historeStates);
    drawStateBox(context, stateBoxMap[name], name, boxType);
  }

  for (const { source, target, angles } of arrowLines) {
    drawEdgeArrowLine(context, stateBoxMap[source], stateBoxMap[target], angles);
  }
}


function test1() {
  /** @type { HTMLCanvasElement } */
  const canvas = document.querySelector("#canvas1");
  const context = canvas.getContext("2d");

  const currentState = "Closed";
  const historeStates = ["Open"];

  /** @type { StateMap } */
  const stateBoxMap = {
    "Unspecified": [50, 130],
    "Open": [157, 130],
    "Closed": [276, 170],
    "Review": [276, 50],
    "Testing": [388, 50],
    "Successed": [496, 50],
    "Publish": [606, 50],
    "Over": [716, 50],
  };

  /** @type { ArrowLine[] } */
  const arrowLines = [
    { source: "Unspecified", target: "Open" },
    { source: "Open", target: "Closed", angles: [20, -20] },
    { source: "Closed", target: "Open", angles: [-55, 40] },
    { source: "Open", target: "Review", angles: [35, -20] },
    { source: "Review", target: "Open" },
    { source: "Review", target: "Testing" },
    { source: "Testing", target: "Successed" },
    { source: "Successed", target: "Publish" },
    { source: "Publish", target: "Over" },
  ];

  drawBackground(canvas, context, 777, 242, 2);
  dragStateBoxMap(context, currentState, historeStates, stateBoxMap, arrowLines);
}

test1();
